/**
 * 
 */
package org.mtdownload.app.loader;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLStreamHandler;
import java.util.Hashtable;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;


/**
 * NOT USED ANYMORE. TOOOO SLOW. WILL DEPLOY THE APP USING THE NORMAL CLASS-LOADER
 * 
 * This class is used to launch the app when deployed as a single .jar file . It knows to look inside 
 * for toher jars, or other resources, as requested.
 * @author sergiu
 */
public class JarClassLoader extends ClassLoader {

	private static final String OUR_NAME="jmtdownload.jar";	
	private static byte[] zipStreamBytes;
	private URLStreamHandler streamHandler=null;
	//this hashtable stores all the resource ever asked for by the JVM
	//on a second call, the bytes stored here are retrieved, instead of looking through all the zip file
	//it eats up memory, but it's faster
	private Hashtable<String,InputStream> loadedResource=new Hashtable<String,InputStream>(100);//1000 initial capacity sounds ... better
	
	
	
	/* (non-Javadoc)
	 * @see java.lang.ClassLoader#findResource(java.lang.String)
	 */
	@Override
	protected URL findResource(String name) {
		System.out.println("find resource:"+name);
		URL url=super.findResource(name);
		if(url==null){
			//JVM didn't found it, so we have to			
			try{
				//we're using our protocol handler to search for the resources
				//BUT, we're doing a little trick here.
				//because we can't just say new JClassRunnerStreamHandler() here (if we do that
				//the JVM will automatically search for it on its own)
				//we will use reflection, witch means, WE will search for it, WE will define it
				//and WE will provide it to the JVM
				if(streamHandler==null){
					streamHandler=(URLStreamHandler)loadClass("org.mtdownload.app.loader.JarClassLoaderStreamHandler").newInstance();
				}
				url=new URL("classrunner",null,-1,name,streamHandler);
			}catch(MalformedURLException exc){
				exc.printStackTrace();
			}catch(ClassNotFoundException exc){
				exc.printStackTrace();
			}catch(IllegalAccessException exc){
				exc.printStackTrace();
			}catch(InstantiationException exc){
				exc.printStackTrace();
			}
		}
		return url;
	}

	/* (non-Javadoc)
	 * @see java.lang.ClassLoader#loadClass(java.lang.String)
	 */
	@Override
	public synchronized Class<?> loadClass(String name) throws ClassNotFoundException {
		System.out.println("load class: "+name);
		Class loadedClass=null;
		/*if(name!=null && name.startsWith("java")){//most likely is a system class
			try{
				//ask the JVM to find it
				loadedClass= findSystemClass(name);
			}catch(ClassNotFoundException ignored){/*}
		}*/
		if(name.equals("org.mtdownload.app.loader.JarClassLoader")){
			return this.getClass();
		}
		
		if(loadedClass==null){
			System.out.println("Searching for "+name);
			//ok, nobody found it yet let's search for it.
			//the name gets passed to us in the form: java.lang.String, we have to make it: java/lang/String
			//TODO: find out a way of speeding up the things. storing the bytes in memory would be
			//one ideea, BUT if the JVM does that too, then why store it twice?
			String className=name.replace('.','/')+".class";
			//System.out.println(className);
			try{
				InputStream classStream=getInputStreamForResource(className);
				if(classStream!=null){
					ByteArrayOutputStream outStr=new ByteArrayOutputStream();
					byte[] data=new byte[1024];
					int read=0;
					while((read=classStream.read(data))>=0){
						outStr.write(data,0,read);
					}
					byte[] theClass=outStr.toByteArray();
					loadedClass=defineClass(name,theClass,0,theClass.length);
				}
			}catch(IOException exc){
				throw new ClassNotFoundException("Archive error",exc);
			}			
		}else{
			//System.out.println("JVM found "+name);
		}
		if(loadedClass==null){
			try{
				//ask the JVM to find it
				loadedClass= findSystemClass(name);
			}catch(ClassNotFoundException ignored){/**/}			
		}
		if(loadedClass==null){
			//if we are STILL null, then ... Houston, we've got a problem
			throw new ClassNotFoundException("Can't find "+name);
		}
		return loadedClass;
	}



	/**
	 * Returns an input stream that points to a specified resource in the 
	 * contained zip file ar in the zip files in the zip file.
	 * this resource can be a class file or any other resource
	 * @param name
	 * @return
	 */
	public InputStream getInputStreamForResource(String name) {				
		InputStream in=null;		
		in=loadedResource.get(name);
		if(in!=null){
			return in;
		}		
		//we're gonna look for it  here, in the file
		ZipEntry entry;
		try{
			ZipInputStream zipStream=new ZipInputStream(new ByteArrayInputStream(zipStreamBytes));
			while((entry=zipStream.getNextEntry())!=null){
			//	System.out.println(entry.getName());					
				if(entry.getName().equals(name)){						
					ByteArrayOutputStream outStr=new ByteArrayOutputStream();
					byte[] data=new byte[1024];
					int read=0;
					while((read=zipStream.read(data))>=0){
						outStr.write(data,0,read);
					}
					byte[] theResource=outStr.toByteArray();
					in=new ByteArrayInputStream(theResource);
				}
				if(entry.getName().toLowerCase().endsWith(".jar")||
					entry.getName().toLowerCase().endsWith(".zip")){
					//oki. we have an entry here that's a zip file.
					//let search through it too, to find the class
					ByteArrayOutputStream outStr=new ByteArrayOutputStream();
					byte[] data=new byte[1024];
					int read=0;
					while((read=zipStream.read(data))>=0){
						outStr.write(data,0,read);
					}
					byte[] theZipFile=outStr.toByteArray();						
					ZipInputStream internalZipStream=new ZipInputStream(new ByteArrayInputStream(theZipFile));
					ZipEntry internalEntry=null;
					while((internalEntry=internalZipStream.getNextEntry())!=null){
						if(internalEntry.getName().equals(name)){						
							ByteArrayOutputStream internalOutStr=new ByteArrayOutputStream();
							byte[] internalData=new byte[1024];
							int r=0;
							while((r=internalZipStream.read(internalData))>=0){
								internalOutStr.write(internalData,0,r);
							}
							byte[] theResource=internalOutStr.toByteArray();
							in=new ByteArrayInputStream(theResource);
						}
						internalZipStream.closeEntry();
					}						
				}
				zipStream.closeEntry();
			}
			zipStream.close();
			zipStream=null;
		}catch(IOException exc){
			exc.printStackTrace();
		}			
		if(in!=null){
			loadedResource.put(name,in);
		}
		if(in==null){
			System.out.println("can't find "+name);
		}
		return in;
	}


	/**
	 * Find the jar. When launched with the -jar jarname.jar option, in the class path there is only
	 * this jar available. so ... load the first file found
	 */
    private static void locateClassFileAndLoadZip(){
        String classPath = System.getProperty("java.class.path");        
        File file = null;
        int index=classPath.indexOf(File.pathSeparatorChar);
        if(index>0){
        	file=new File(classPath.substring(0,index));
        }else{
        	file=new File(classPath);
        }
        System.out.println(file);
        if(file.exists()){
            try{
            	InputStream containedZipFileStream=new FileInputStream(file);
            	ByteArrayOutputStream byteOutStream=new  ByteArrayOutputStream ();
            	byte[] data=new byte[1024];
            	int read=0;
            	while((read=containedZipFileStream.read(data))>=0){
            		byteOutStream.write(data,0,read);
            	}
                zipStreamBytes=byteOutStream.toByteArray();                                 	
            }catch(IOException _ex){
                        _ex.printStackTrace();
            }
        }
    }
	
	/**
	 * @param args the application arguments
	 */
	public static void main(String[] args) {
		locateClassFileAndLoadZip();
		JarClassLoader loader=new JarClassLoader();
		try{
			Class mainClass=loader.loadClass("org.mtdownload.app.MTDownload");
			Method mainMethod=mainClass.getMethod("main",new Class[]{String[].class});
			Object[] mainClassArgs=new Object[1];
			mainClassArgs[0]=args;
			mainMethod.invoke(null,mainClassArgs);
		}catch(Exception exc){
			exc.printStackTrace();
		}
		
	}

}
